# !/usr/bin/env python
#
# Copyright 2008 CPedia.com.
#
# New portions Copyright 2009, 2010 Rob Stenzinger 
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

__author__ = 'Ping Chen, Rob Stenzinger'

import pickle

import logging
import datetime
import time

import urllib
import random
import cgi
import simplejson
import hashlib

from rocket import utils

from google.appengine.ext import search
from google.appengine.api import memcache
from google.appengine.ext import db
from google.appengine.api import users
from google.appengine.api import datastore_types

import parsedatetime.parsedatetime as pdt 
import parsedatetime.parsedatetime_consts as pdc 

from pyparsing import *

"""
=============================================
working on the blog post embedded features ... 
=============================================
"""


comic_wrapper_start = ""
comic_wrapper_end = ""

def convertToHTML_image(s,l,t):
    return "<div class='image'><img %s /></div>" % t[0]

def imagePlaceholderToImageHTML(s,l,t):
    return "<img %s />" % t[0]

def keepComicImage(s,l,t):
    return t[2]

#comicLink = QuotedString("[[img comic", endQuoteChar="]]").setParseAction(convertToHTML_comic)
imageLink = QuotedString("[[img ", endQuoteChar="]]").setParseAction(convertToHTML_image)
imageLink2 = QuotedString("[[img ", endQuoteChar="]]").setParseAction(imagePlaceholderToImageHTML)

removeText = replaceWith("")
comicTag = CaselessLiteral("[comic]") + SkipTo(CaselessLiteral("[/comic]")) + CaselessLiteral("[/comic]") #.setParseAction(removeComicTag) #QuotedString("[comic]", endQuoteChar="[/comic]").setParseAction(removeComicTag)
comicTag.setParseAction(removeText)

comicTagInside = SkipTo(CaselessLiteral("[comic]")) + CaselessLiteral("[comic]") + SkipTo(CaselessLiteral("[/comic]")) + CaselessLiteral("[/comic]") + SkipTo(stringEnd)
comicTagInside.setParseAction(keepComicImage)

def process_blog_section_text(content):
    result = comicTag.transformString(content)
    if result == content:
        return content
    else:
        return result

def process_blog_section_comic(content):
    result = comicTagInside.transformString(content)
    if result == content:
        return ""
    else:
        result = imageLink2.transformString(result)
        return result
    
def process_blog_content_new(blog):
    # comic:
    # image
    # todo:
    if not blog.content:
        return None
        
    # speed it up... look for existence of tags to parse, skip if not needed
    if blog.content.find("[comic]") > -1:    
        # video, audio, game...
        content_comic = process_blog_section_comic(blog.content)
        content_text = process_blog_section_text(blog.content)
        document = {
        "featured":blog.entrytype,
        "text":content_text,
        "comic":content_comic,
        }
    else:
        document = {
        "featured":blog.entrytype,
        "text":blog.content,
        "comic":None,
        }
        
    return document

config_post_types = ['post','page','comic']
config_post_timeline_types = ['post','comic']
config_post_default = 'post'

config_post_status_types = ["draft", "published", "scheduled"]
config_post_status_default = "draft"
config_post_status_default_ready = "published"
config_post_status_scheduled = "scheduled"

config_response_status_types = ["approved", "pending", "spam"]
config_response_status_default = "pending"
config_response_status_default_ready = "approved"
config_response_status_default_pending = "pending"


#from util import getGravatarUrl

def to_dict(model_obj, attr_list, init_dict_func=None):
    """Converts Model properties into various formats.

    Supply a init_dict_func function that populates a
    dictionary with values.  In the case of db.Model, this
    would be something like _to_entity().  You may also
    designate more complex properties in attr_list, like
      "counter.count"
    Each object in the chain will be retrieved.  In the
    example above, the counter object will be retrieved
    from model_obj's properties.  And then counter.count
    will be retrieved.  The value returned will have a
    key set to the last name in the chain, e.g. 'count'
    in the above example.
    """
    values = {}
    init_dict_func(values)
    for token in attr_list:
        elems = token.split('.')
        value = getattr(model_obj, elems[0])
        for elem in elems[1:]:
            value = getattr(value, elem)
        values[elems[-1]] = value
    if model_obj.is_saved():
        values['key'] =  str(model_obj.key())
    return values

# Format for conversion of datetime to JSON
DATE_FORMAT = "%Y-%m-%d"
TIME_FORMAT = "%H:%M:%S"

def replace_datastore_types(entity):
    """Replaces any datastore types in a dictionary with standard types.

    Passed-in entities are assumed to be dictionaries with values that
    can be at most a single list level.  These transformations are made:
      datetime.datetime      -> string
      db.Key                 -> key hash suitable for regenerating key
      users.User             -> dict with 'nickname' and 'email'
    TODO -- GeoPt when needed
    """
    def get_replacement(value):
        if isinstance(value, datetime.datetime):
            return value.strftime("%s %s" % (DATE_FORMAT, TIME_FORMAT))
        elif isinstance(value, datetime.date):
            return value.strftime(DATE_FORMAT)
        elif isinstance(value, datetime.time):
            return value.strftime(TIME_FORMAT)
        elif isinstance(value, datastore_types.Key):
            return str(value)
    
        elif isinstance(value, db.ReferenceProperty):
            return str(value)
        elif isinstance(value, db.UserProperty):
            return str(value)
        elif isinstance(value, db.DateTimeProperty):
            return value.strftime(DATE_FORMAT)
                
        elif isinstance(value, users.User):
            return { 'nickname': value.nickname(),
                     'email': value.email(),
                     'user_id':value.user_id() }
            
        else:
            return None

    for key, value in entity.iteritems():
        if isinstance(value, list):
            new_list = []
            for item in value:
                new_value = get_replacement(item)
                new_list.append(new_value or item)
            entity[key] = new_list
        else:
            new_value = get_replacement(value)
            if new_value:
                entity[key] = new_value

class SerializableModel(db.Model):
    """Extends Model to have json and possibly other serializations

    Use the class variable 'json_does_not_include' to declare properties
    that should *not* be included in json serialization.
    TODO -- Complete round-tripping
    """
    json_does_not_include = []
    json_properties = []
    json_properties_lightweight = []
    
    timestamp = db.DateTimeProperty(auto_now=True)

    def to_json(self, lightweight):
        def to_entity(entity):
            """Convert datastore types in entity to
               JSON-friendly structures."""
            #self._to_entity(entity)
            #for skipped_property in self.__class__.json_does_not_include:
            #    del entity[skipped_property]
            #output = {}
            replace_datastore_types(entity)
            
        #values = to_dict(self, json_properties, to_entity)
        #return simplejson.dumps(values)   #simplejson.dumps will be applied when do the rpc call.
        #return simplejson.dumps(to_dict(self, self.json_properties, to_entity))
        if lightweight == True:
            return to_dict(self, self.json_properties_lightweight, to_entity)
        else:
            return to_dict(self, self.json_properties, to_entity)

class MemcachedModel(SerializableModel):
#All the query to this model need to be self-stored.
#The dict need to set a unique key, and the value must be db.Query() object.
    querys = {}
    list_includes = []
    
    def delete_cached_list(self):
        memcache_list_keys = self.__class__.memcache_list_key()
        if memcache_list_keys is not None and len(memcache_list_keys) > 0:
            memcache.delete_multi(memcache_list_keys)

    def delete(self):
        super(MemcachedModel, self).delete()
        self.delete_cached_list()
        if self.is_saved():
            memcache.delete(self.__class__.memcache_object_key(self.key()))

    def put(self):
        key = super(MemcachedModel, self).put()
        self.delete_cached_list()
        memcache.set(self.__class__.memcache_object_key(key),self)
        return key

    def _to_repr(self):
        return repr(to_dict(self, self.__class__.list_includes,
                    self._to_entity))

    @classmethod
    def get_or_insert(cls, key_name, **kwds):
        obj = super(MemcachedModel, cls).get_or_insert(key_name, **kwds)
        self.delete_cached_list()
        return obj

    @classmethod
    def memcache_list_key(cls):
        keys =  [cls.__name__ +"_list_" +  query_key  for query_key in cls.querys]
        keys += ['PS_' + cls.__name__ + '_ALL']
        return keys

    @classmethod
    def memcache_object_key(cls,primary_key):
        return cls.__name__ + '_' + str(primary_key)

    @classmethod
    def get_cached(cls,primary_key,nocache=False):
        key_ = cls.__name__ + "_" + primary_key
        try:
            result = memcache.get(key_)
        except Exception:
            result = None
        if nocache or result is None:
            result = cls.get(primary_key)
            memcache.set(key=key_, value=result)
        return result

    @classmethod
    def memcache_all_key(cls):
        return 'PS_' + cls.__name__ + '_ALL'

    @classmethod
    def list(cls, nocache=False):
        """Returns a list of up to 1000 dicts of model values.
           Unless nocache is set to True, memcache will be checked first.
        Returns:
          List of dicts with each dict holding an entities property names
          and values.
        """
        return None
        """    
                list_repr = memcache.get(cls.memcache_all_key())
                if nocache or list_repr is None:
                    #q = db.Query(cls)
                    #objs = q.fetch(limit=1000)
                    
                    objs = [cls.__name__].fetch(limit=1000)
                    list_repr = '[' + ','.join([obj._to_repr() for obj in objs]) + ']'
                    memcache.set(cls.memcache_all_key(), list_repr)
                    
                return eval(list_repr)
        """    
        
    @classmethod
    def get_cached_list(cls, query_key,params=[],nocache=False):
        """Return the cached list with the specified key.
        User must keep the key unique, and the query must
        be same instance of the class .
        """
        key_ = cls.__name__ +"_list_" + query_key
        try:
            result = memcache.get(key_)
        except Exception:
            result = None
        if nocache or result is None:
            if query_key in cls.querys:
                params_ = [cls.querys[query_key]] + params
                query = db.GqlQuery(*params_ )
                result = query.fetch(1000)
                memcache.add(key=key_, value=result)
            else:
                raise Exception("Query for object list does not define in the Class Model.")
        return result
#
class Archive(MemcachedModel):
    json_properties = ["monthyear","weblogcount","date"]
    json_properties_lightweight = ["monthyear","weblogcount","date"]
    monthyear = db.StringProperty(multiline=False)
    """July 2008"""
    weblogcount = db.IntegerProperty(default=0)
    date = db.DateTimeProperty(auto_now_add=True)

class Tag(MemcachedModel):
    json_properties = ["tag","entrycount"]
    json_properties_lightweight = ["tag","entrycount"]
    tag = db.StringProperty(multiline=False)
    entrycount = db.IntegerProperty(default=0)
    valid = db.BooleanProperty(default = True)

# added by Rob Stenzinger:    
class Book(MemcachedModel):
    json_properties = ["title","description","sortrank","entrycount","cover"]
    json_properties_lightweight = ["title","description","sortrank","entrycount","cover"]
    title = db.StringProperty(multiline=False)
    description = db.StringProperty(multiline=False)
    cover = db.TextProperty()
    sortrank = db.IntegerProperty(default=0)    
    entrycount = db.IntegerProperty(default=0)

# added by Rob Stenzinger:    
class Chapter(MemcachedModel):
    json_properties = ["title","description","sortrank","entrycount","cover"]
    json_properties_lightweight = ["title","description","sortrank","entrycount","cover"]    
    title = db.StringProperty(multiline=False)
    book = db.ReferenceProperty(Book)
    description = db.StringProperty(multiline=False)
    cover = db.TextProperty()
    sortrank = db.IntegerProperty(default=0)
    entrycount = db.IntegerProperty(default=0)

class Weblog(SerializableModel):
    json_properties = ["permalink","title","date","entrytype","content","json_document","status","book","chapter"]
    json_properties_lightweight = ["permalink","title","date","entrytype","status","book","chapter"]
    permalink = db.StringProperty()
    title = db.StringProperty()
    content = db.TextProperty()
    date = db.DateTimeProperty()
    author = db.UserProperty()
    authorEmail = db.EmailProperty()
    catalog = db.StringProperty()
    lastCommentedDate = db.DateTimeProperty()
    commentcount = db.IntegerProperty(default=0)
    lastModifiedDate = db.DateTimeProperty()
    lastModifiedBy = db.UserProperty()
    tags = db.ListProperty(db.Category)
    monthyear = db.StringProperty(multiline=False)
    entrytype = db.StringProperty(multiline=False,default=config_post_default,choices=config_post_types)
    _weblogId = db.IntegerProperty()   # for data migration from the mysql system
    
    # added by Rob Stenzinger: 
    chapter = db.StringProperty(default='',multiline=False)
    book = db.StringProperty(default='',multiline=False)
    status = db.StringProperty(multiline=False,default=config_post_status_default,choices=config_post_status_types)
    json_document = db.TextProperty() # JSON serialized, parsed content

    def relative_permalink(self):
        if self.entrytype == 'post':
            return self.date.strftime('%Y/%m/')+ self.permalink
        else:
            return self.permalink

    def full_permalink(self):
        if self.entrytype == 'post':
            return '/' + self.date.strftime('%Y/%m/')+ self.permalink
        else:
            return '/'+ self.permalink

    def get_tags(self):
        '''comma delimted list of tags'''
        return ','.join([urllib.unquote(tag.encode('utf8')) for tag in self.tags])

    def set_tags(self, tags):
        if tags:
            self.tags = [db.Category(urllib.quote(tag.strip().encode('utf8'))) for tag in tags.split(',')]

    tags_commas = property(get_tags,set_tags)

    #for data migration
    def update_archive(self,action):
        """Checks to see if there is a month-year entry for the
        month of current blog, if not creates it and increments count
        action: 0 update the blog.
                1 add a new blog.
                2 delete the blog.
        """
        try:
            my = self.date.strftime('%B %Y') # July 2008
            archive = Archive.all().filter('monthyear',my).fetch(10)
            if archive == []:
                archive = Archive(monthyear=my,date=self.date,weblogcount=1)
                archive.put()
            else:
                if action==1:
                # ratchet up the count
                    archive[0].weblogcount += 1
                    archive[0].put()
                elif action == 2:
                    archive[0].weblogcount -= 1
                    if archive[0].weblogcount == 0:
                        archive[0].delete()
                    else:
                        archive[0].put()
        except:
            #todo:
            pass
        

    def update_books(self,action):
        """
        action: 0 update the blog.
                1 add a new blog.
                2 delete the blog.
        """
        if self.book:
            books = Book.all().filter('title =',self.book).fetch(1)
            if books == []:
                book = Book(title=self.book,entrycount=1)
                book.put()
            else:
                if action == 1:
                    books[0].entrycount += 1
                    books[0].put()
                elif action == 2:
                    books[0].entrycount -= 1
                    if books[0].entrycount == 0:
                        books[0].delete()
                    else:
                        books[0].put()
                        
    def update_chapters(self,action):
        """
        action: 0 update the blog.
                1 add a new blog.
                2 delete the blog.
        """
        if self.chapter:
            chapters = Chapter.all().filter('title =',self.chapter).fetch(1)
            if chapters == []:
                chapter = Chapter(title=self.chapter,entrycount=1)
                if self.book:
                    try:
                        book = Book.all().filter('title =',self.book).fetch(1)[0]
                        chapter.book = book
                    except:
                        #todo: log this...
                        pass
                chapter.put()
            else:
                if action == 1:
                    chapters[0].entrycount += 1
                    chapters[0].put()
                elif action == 2:
                    chapters[0].entrycount -= 1
                    if chapters[0].entrycount == 0:
                        chapters[0].delete()
                    else:
                        chapters[0].put()                    

    def update_tags(self,action):
        """Update Tag cloud info
        action: 0 update the blog.
                1 add a new blog.
                2 delete the blog.
        """
        if self.tags:
            for tag_ in self.tags:
            #tag_ = tag.encode('utf8')
                #todo: this is likely a "contains" query so it's not a perfect match... 
                tags = Tag.all().filter('tag =',tag_).fetch(10)
                if tags == []:
                    tagnew = Tag(tag=tag_,entrycount=1)
                    tagnew.put()
                else:
                    if action==1:
                        tags[0].entrycount+=1
                        tags[0].put()
                    elif action ==2:
                        tags[0].entrycount-=1
                        if tags[0].entrycount == 0:
                            tags[0].delete()
                        else:
                            tags[0].put()

    def save(self):
        self.update_tags(1)
        self.update_books(1)
        self.update_chapters(1)

        #detect change:
        haskey = False
        changed = False
        try:
            if self.is_saved():
                stored_record = db.get(self.key())
                if stored_record.content != self.content:
                    changed = True
        except:
            haskey = False
            changed = True

        if self.entrytype != "page":
            self.update_archive(1)

        if self.date is None:
            self.date = datetime.datetime.now()
        #else:
        #    self.date = parseDate(self.date)
            
        #if self.content and changed == True:
        self.json_document = simplejson.dumps(process_blog_content_new(self))
        
        try:    
            my = self.date.strftime('%B %Y') # July 2008
            self.monthyear = my
        except:
            #todo:
            pass        
        
        self.put()

    def update(self):
        self.update_tags(0)
        self.update_books(0)
        self.update_chapters(0)
        if self.entrytype != "page":
            self.update_archive(0)
        if self.content:
            self.json_document = simplejson.dumps(process_blog_content_new(self))
        self.put()

    def delete(self):
        self.update_tags(2)
        self.update_books(2)
        self.update_chapters(2)
        if self.entrytype != "page":
            self.update_archive(2)
        super(Weblog, self).delete()


class WeblogReactions(SerializableModel):
    json_properties = ["author","authorEmail","authorWebsite","content","reply_count","reply_depth","status"]
    json_properties_lightweight = ["author","authorEmail","authorWebsite","reply_count","reply_depth","status"]
    weblog = db.ReferenceProperty(Weblog)
    user = db.StringProperty()
    date = db.DateTimeProperty() #auto_now_add=True
    author = db.UserProperty()
    authorEmail = db.EmailProperty()
    authorWebsite = db.StringProperty()
    userIp = db.StringProperty()
    content = db.TextProperty()
    lastModifiedDate = db.DateTimeProperty()
    lastModifiedBy = db.UserProperty()
    _weblogReactionId = db.IntegerProperty()   ##for data migration from the mysql system
    reply_to = db.SelfReferenceProperty(collection_name="replies")
    is_reply = db.BooleanProperty(default=False)
    reply_count = db.IntegerProperty(default=0)
    reply_flat_list = db.StringProperty(multiline=False, default="")
    reply_depth = db.IntegerProperty(default=0)
    status = db.StringProperty(multiline=False,default=config_response_status_default,choices=config_response_status_types)
    #user_gravatar_url = db.StringProperty(multiline=False)
 

    def save(self): 
        
        do_save = False
        
        #todo... review this, have a more direct admin config option... 
        if self.author:
            do_save = True
        elif self.status == config_response_status_default_ready:
            do_save = True  
        elif len(self.authorEmail) > 0:
            approved_list = WeblogReactions.all().filter('authorEmail =',self.authorEmail)
            approved_list.filter('status = ', config_response_status_default_ready).fetch(1)

            if approved_list == []:
                if self.status != "spam":
                    self.status = config_response_status_default_pending
                    do_save = True
                else:
                    do_save = False                    
            else:
                do_save = True         
                

        if do_save == True:
            reply_list = ""
            
            if self.reply_to is not None:
                self.is_reply = True
                #reply_to = db.get(self.reply_to)
                self.reply_to.reply_count += 1
                self.reply_to.put()
                #reply_list = self.reply_to.reply_flat_list + "/" + str(self.reply_to.key())            
                #reply_list = self.reply_to.reply_flat_list + "/" + str(self.reply_to.key().id()) + "/"
                reply_list = self.reply_to.reply_flat_list + "/" 
                            
            if self.date is None:
                self.date = datetime.datetime.now()

            self.put()

            reply_list = reply_list + str(self.key().id())
            self.reply_depth = len(reply_list.split("/")) - 1
            self.reply_flat_list = reply_list
            
            self.put()            
                
            if self.weblog is not None:
                self.weblog.lastCommentedDate = self.date
                self.weblog.commentcount += 1
                self.weblog.put()
                
        return do_save
            
    def delete(self):
        child_reactions = WeblogReactions.all()
        child_reactions.filter("reply_to =", self.key())
        super(WeblogReactions, self).delete()
        for reaction in child_reactions:
            reaction.delete()

# added by Rob Stenzinger:     
class Attachment(SerializableModel):
    uploader = db.UserProperty()
    date = db.DateTimeProperty()
    filename = db.StringProperty()
    filemeta = db.StringProperty()
    
# added by Rob Stenzinger:     
class Images(Attachment):
    image = db.BlobProperty()

# added by Rob Stenzinger:     
class Flash(Attachment):
    swf = db.BlobProperty()

# added by Rob Stenzinger:     
class Audio(Attachment):
    mp3 = db.BlobProperty()

# added by Rob Stenzinger:     
class Video(Attachment):
    mp4 = db.BlobProperty()
    wmv = db.BlobProperty()
    xvid = db.BlobProperty()
    size = db.StringProperty(multiline=False,default=config_post_status_default,choices=['small','large','hd'])

# added by Rob Stenzinger:     
class Feeds(SerializableModel):
    title = db.StringProperty()
    feed = db.StringProperty()
    order = db.IntegerProperty()
    valid = db.BooleanProperty(default = True)


json_lightweight = False
def to_json(o):
    try:
        return o.to_json(json_lightweight)
    except Exception,e:        
        if isinstance(o, datetime.datetime):
            return o.strftime("%s %s" % (DATE_FORMAT, TIME_FORMAT))
        elif isinstance(o, datetime.date):
            return o.strftime(DATE_FORMAT)
        elif isinstance(o, datetime.time):
            return o.strftime(TIME_FORMAT)
        elif isinstance(o, datastore_types.Key):
            return str(o)
    
        elif isinstance(o, db.ReferenceProperty):
            return str(o)
        elif isinstance(o, db.UserProperty):
            return str(o)
        elif isinstance(o, db.DateTimeProperty):
            return o.strftime(DATE_FORMAT)
                
        elif isinstance(o, users.User):
            return { 'nickname': value.nickname(),
                     'email': value.email(),
                     'user_id':value.user_id() }            
        else:
            return None        

# added by Rob Stenzinger:         
def parseDate(s):
    try:
        result = utils.from_iso(s)
        if isinstance(result, datetime.datetime):
            return result
    except:
        pass
    
    if isinstance(s, datetime.datetime):
        return s
    else:
        # create an instance of Constants class so we can override some of the defaults
        c = pdc.Constants()
        # create an instance of the Calendar class and pass in our Constants # object instead of letting it create a default
        p = pdt.Calendar(c)
        result = p.parse(s) 
        return datetime.datetime(time.struct_time(result[0]).tm_year,time.struct_time(result[0]).tm_mon,time.struct_time(result[0]).tm_mday,time.struct_time(result[0]).tm_hour,time.struct_time(result[0]).tm_min,time.struct_time(result[0]).tm_sec)
    